Skip to content

Conversation

@MritunjayTiwari14
Copy link
Contributor

@MritunjayTiwari14 MritunjayTiwari14 commented Oct 30, 2025

Fixes #1953.

The Scale down and Scale up as well as Ink Splash animation occurs after a finite delay of 100ms. Mainly due to GestureDetector used inside AnimatedScaleOnTap.

This can be fixed by using Listener hence replacing the GestureDetector inside AnimatedScaleOnTap.

Discussion: #mobile > issue in responsiveness of home bottom navbar icons
Design: Figma Design

Comparison Through Video of Fix-up

Before

implementation.mp4

After

fixup.mp4

@gnprice gnprice added the maintainer review PR ready for review by Zulip maintainers label Oct 30, 2025
Copy link
Collaborator

@chrisbobbe chrisbobbe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Comments below.

check(scale).equals(expectedScale);
}

testWidgets('Animation happen instantly when tap down', (tester) async {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I run this test without your change in lib/widgets/button.dart, it still passes. That means it's not doing its job.

Comment on lines 306 to 310
return Listener(
behavior: HitTestBehavior.translucent,
onTapDown: (_) => _changeScale(widget.scaleEnd),
onTapUp: (_) => _changeScale(1),
onTapCancel: () => _changeScale(1),
onPointerDown: (_) => _changeScale(widget.scaleEnd),
onPointerUp: (_) => _changeScale(1),
onPointerCancel: (_) => _changeScale(1),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From reading the docs for GestureDetector.onTapDown and friends, it looks like one of the helpful features of that higher-level API is that it only considers pointer events from a "primary button". I don't know how common it is e.g. to use a stylus with multiple buttons, but this seems like functionality that would be helpful to keep.

The event passed to the Listener.onPointerDown callback (etc.) has a buttons field. So in each of the handlers, let's check whether the event's buttons field equals kPrimaryButton, and only respond if it does.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, the name and dartdoc of this AnimatedScaleOnTap should be updated to match the new behavior: it's no longer about a tap gesture, which is only recognized after 100ms. For example:

/// Apply [Transform.scale] to the child widget on primary pointer-down,
/// and reset its scale on -up or -cancel, with animated transitions.
class AnimatedScaleOnPointerDown extends StatefulWidget {

@chrisbobbe chrisbobbe self-assigned this Dec 2, 2025
@MritunjayTiwari14
Copy link
Contributor Author

Thank you @chrisbobbe for the Review, added a timeout of 50 millisecond to pumpAndSettle to make sure in the test that animation happens almost instantly. Checked it with the previous code of button.dart that it failed, the updated code in button.dart as proposed in this PR does pass those test. PTAL.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR eliminates the 100ms delay in scale animations by replacing GestureDetector with Listener in button touch feedback. The widget has been renamed from AnimatedScaleOnTap to AnimatedScaleOnPrimaryPointerDown to better reflect its implementation, which now responds immediately to pointer events rather than waiting for gesture recognition.

Key changes:

  • Replaced GestureDetector with lower-level Listener API for immediate touch response
  • Renamed widget to AnimatedScaleOnPrimaryPointerDown to reflect the implementation change
  • Added primary button detection logic to only respond to primary pointer button presses

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
lib/widgets/button.dart Implemented Listener-based touch handling with primary button detection, replacing gesture-based approach
lib/widgets/home.dart Updated all widget references to use the renamed AnimatedScaleOnPrimaryPointerDown
test/widgets/button_test.dart Added test verifying instant animation response on pointer down/up

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

if(_pressedButton & kPrimaryButton != 0) _changeScale(widget.scaleEnd);
},
onPointerUp: (_) {
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _pressedButton state should be reset to 0 in onPointerUp as well, not just in onPointerCancel. Without this, if the user presses the primary button and releases it, then presses a secondary button, the widget may incorrectly maintain the previous button state. Add _pressedButton = 0; after the scale change.

Suggested change
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
_pressedButton = 0;

Copilot uses AI. Check for mistakes.
check(renderObject).size.equals(Size.square(40));
});

group('AnimatedScaleOnTap', () {
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test group name should be updated to match the renamed widget. Change 'AnimatedScaleOnTap' to 'AnimatedScaleOnPrimaryPointerDown'.

Suggested change
group('AnimatedScaleOnTap', () {
group('AnimatedScaleOnPrimaryPointerDown', () {

Copilot uses AI. Check for mistakes.
Comment on lines 312 to 319
if(_pressedButton & kPrimaryButton != 0) _changeScale(widget.scaleEnd);
},
onPointerUp: (_) {
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
},
onPointerCancel: (_) {
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect operator precedence. The != operator has higher precedence than &, so this expression is evaluated as _pressedButton & (kPrimaryButton != 0) instead of (_pressedButton & kPrimaryButton) != 0. Add parentheses around the bitwise AND operation: if ((_pressedButton & kPrimaryButton) != 0).

Suggested change
if(_pressedButton & kPrimaryButton != 0) _changeScale(widget.scaleEnd);
},
onPointerUp: (_) {
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
},
onPointerCancel: (_) {
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
if ((_pressedButton & kPrimaryButton) != 0) _changeScale(widget.scaleEnd);
},
onPointerUp: (_) {
if ((_pressedButton & kPrimaryButton) != 0) _changeScale(1);
},
onPointerCancel: (_) {
if ((_pressedButton & kPrimaryButton) != 0) _changeScale(1);

Copilot uses AI. Check for mistakes.
Comment on lines 312 to 319
if(_pressedButton & kPrimaryButton != 0) _changeScale(widget.scaleEnd);
},
onPointerUp: (_) {
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
},
onPointerCancel: (_) {
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect operator precedence. The != operator has higher precedence than &, so this expression is evaluated as _pressedButton & (kPrimaryButton != 0) instead of (_pressedButton & kPrimaryButton) != 0. Add parentheses around the bitwise AND operation: if ((_pressedButton & kPrimaryButton) != 0).

Suggested change
if(_pressedButton & kPrimaryButton != 0) _changeScale(widget.scaleEnd);
},
onPointerUp: (_) {
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
},
onPointerCancel: (_) {
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
if((_pressedButton & kPrimaryButton) != 0) _changeScale(widget.scaleEnd);
},
onPointerUp: (_) {
if((_pressedButton & kPrimaryButton) != 0) _changeScale(1);
},
onPointerCancel: (_) {
if((_pressedButton & kPrimaryButton) != 0) _changeScale(1);

Copilot uses AI. Check for mistakes.
Comment on lines 312 to 319
if(_pressedButton & kPrimaryButton != 0) _changeScale(widget.scaleEnd);
},
onPointerUp: (_) {
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
},
onPointerCancel: (_) {
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
Copy link

Copilot AI Dec 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incorrect operator precedence. The != operator has higher precedence than &, so this expression is evaluated as _pressedButton & (kPrimaryButton != 0) instead of (_pressedButton & kPrimaryButton) != 0. Add parentheses around the bitwise AND operation: if ((_pressedButton & kPrimaryButton) != 0).

Suggested change
if(_pressedButton & kPrimaryButton != 0) _changeScale(widget.scaleEnd);
},
onPointerUp: (_) {
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
},
onPointerCancel: (_) {
if(_pressedButton & kPrimaryButton != 0) _changeScale(1);
if((_pressedButton & kPrimaryButton) != 0) _changeScale(widget.scaleEnd);
},
onPointerUp: (_) {
if((_pressedButton & kPrimaryButton) != 0) _changeScale(1);
},
onPointerCancel: (_) {
if((_pressedButton & kPrimaryButton) != 0) _changeScale(1);

Copilot uses AI. Check for mistakes.
Fixes zulip#1953.
Latter provides instant scaleDown animation on tap,
unlike GestureDetector which has a `kPressTimeOut` causing
delay of 100ms
    https://main-api.flutter.dev/flutter/gestures/kPressTimeout-constant.html
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintainer review PR ready for review by Zulip maintainers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

button: Delayed tap feedback on bottom nav and menu buttons

3 participants